// Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL

package javax.jmdns.impl;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Iterator;

import android.util.Log;

/**
 * DNS record
 * @version %I%, %G%
 * @author Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch
 */
public abstract class DNSRecord extends DNSEntry {
   public final static String TAG = DNSRecord.class.toString();
   private int ttl;
   private long created;

   /**
    * This source is mainly for debugging purposes, should be the address that
    * sent this record.
    */
   private InetAddress source;

   /**
    * Create a DNSRecord with a name, type, clazz, and ttl.
    */
   DNSRecord(String name, int type, int clazz, int ttl) {
      super(name, type, clazz);
      this.ttl = ttl;
      this.created = System.currentTimeMillis();
   }

   /**
    * True if this record is the same as some other record.
    */
   public boolean equals(Object other) {
      return (other instanceof DNSRecord) && sameAs((DNSRecord) other);
   }

   /**
    * True if this record is the same as some other record.
    */
   boolean sameAs(DNSRecord other) {
      return super.equals(other) && sameValue((DNSRecord) other);
   }

   /**
    * True if this record has the same value as some other record.
    */
   abstract boolean sameValue(DNSRecord other);

   /**
    * True if this record has the same type as some other record.
    */
   boolean sameType(DNSRecord other) {
      return type == other.type;
   }

   /**
    * Handles a query represented by this record.
    * @return Returns true if a conflict with one of the services registered with
    *         JmDNS or with the hostname occured.
    */
   abstract boolean handleQuery(JmDNSImpl dns, long expirationTime);

   /**
    * Handles a responserepresented by this record.
    * @return Returns true if a conflict with one of the services registered with
    *         JmDNS or with the hostname occured.
    */
   abstract boolean handleResponse(JmDNSImpl dns);

   /**
    * Adds this as an answer to the provided outgoing datagram.
    */
   abstract DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out)
            throws IOException;

   /**
    * True if this record is suppressed by the answers in a message.
    */
   boolean suppressedBy(DNSIncoming msg) {
      try {
         for (int i = msg.numAnswers; i-- > 0;) {
            if (suppressedBy((DNSRecord) msg.answers.get(i))) {
               return true;
            }
         }
         return false;
      } catch (ArrayIndexOutOfBoundsException e) {
         Log.d(TAG, "suppressedBy() message " + msg + " exception ");
         // msg.print(true);
         return false;
      }
   }

   /**
    * True if this record would be supressed by an answer. This is the case if
    * this record would not have a significantly longer TTL.
    */
   boolean suppressedBy(DNSRecord other) {
      if (sameAs(other) && (other.ttl > ttl / 2)) {
         return true;
      }
      return false;
   }

   /**
    * Get the expiration time of this record.
    */
   long getExpirationTime(int percent) {
      return created + (percent * ttl * 10L);
   }

   /**
    * Get the remaining TTL for this record.
    */
   int getRemainingTTL(long now) {
      return (int) Math.max(0, (getExpirationTime(100) - now) / 1000);
   }

   /**
    * Check if the record is expired.
    */
   public boolean isExpired(long now) {
      return getExpirationTime(100) <= now;
   }

   /**
    * Check if the record is stale, ie it has outlived more than half of its
    * TTL.
    */
   boolean isStale(long now) {
      return getExpirationTime(50) <= now;
   }

   /**
    * Reset the TTL of a record. This avoids having to update the entire record
    * in the cache.
    */
   void resetTTL(DNSRecord other) {
      created = other.created;
      ttl = other.ttl;
   }

   /**
    * Write this record into an outgoing message.
    */
   abstract void write(DNSOutgoing out) throws IOException;

   /**
    * Address record.
    */
   public static class Address extends DNSRecord {
      InetAddress addr;

      Address(String name, int type, int clazz, int ttl, InetAddress addr) {
         super(name, type, clazz, ttl);
         this.addr = addr;
      }

      Address(String name, int type, int clazz, int ttl, byte[] rawAddress) {
         super(name, type, clazz, ttl);
         try {
            this.addr = InetAddress.getByAddress(rawAddress);
         } catch (UnknownHostException exception) {
            Log.d(TAG, "Address() exception ", exception);
         }
      }

      void write(DNSOutgoing out) throws IOException {
         if (addr != null) {
            byte[] buffer = addr.getAddress();
            if (DNSConstants.TYPE_A == type) {
               // If we have a type A records we should answer with a IPv4
               // address
               if (addr instanceof Inet4Address) {
                  // All is good
               } else {
                  // Get the last four bytes
                  byte[] tempbuffer = buffer;
                  buffer = new byte[4];
                  System.arraycopy(tempbuffer, 12, buffer, 0, 4);
               }
            } else {
               // If we have a type AAAA records we should answer with a IPv6
               // address
               if (addr instanceof Inet4Address) {
                  byte[] tempbuffer = buffer;
                  buffer = new byte[16];
                  for (int i = 0; i < 16; i++) {
                     if (i < 11) {
                        buffer[i] = tempbuffer[i - 12];
                     } else {
                        buffer[i] = 0;
                     }
                  }
               }
            }
            int length = buffer.length;
            out.writeBytes(buffer, 0, length);
         }
      }

      boolean same(DNSRecord other) {
         return ((sameName(other)) && ((sameValue(other))));
      }

      boolean sameName(DNSRecord other) {
         return name.equalsIgnoreCase(((Address) other).name);
      }

      boolean sameValue(DNSRecord other) {
         return addr.equals(((Address) other).getAddress());
      }

      public InetAddress getAddress() {
         return addr;
      }

      /**
       * Creates a byte array representation of this record. This is needed for
       * tie-break tests according to draft-cheshire-dnsext-multicastdns-04.txt
       * chapter 9.2.
       */
      private byte[] toByteArray() {
         try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream(bout);
            dout.write(name.getBytes("UTF8"));
            dout.writeShort(type);
            dout.writeShort(clazz);
            // dout.writeInt(len);
            byte[] buffer = addr.getAddress();
            for (int i = 0; i < buffer.length; i++) {
               dout.writeByte(buffer[i]);
            }
            dout.close();
            return bout.toByteArray();
         } catch (IOException e) {
            throw new InternalError();
         }
      }

      /**
       * Does a lexicographic comparison of the byte array representation of
       * this record and that record. This is needed for tie-break tests
       * according to draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2.
       */
      private int lexCompare(DNSRecord.Address that) {
         byte[] thisBytes = this.toByteArray();
         byte[] thatBytes = that.toByteArray();
         for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++) {
            if (thisBytes[i] > thatBytes[i]) {
               return 1;
            } else {
               if (thisBytes[i] < thatBytes[i]) {
                  return -1;
               }
            }
         }
         return thisBytes.length - thatBytes.length;
      }

      /**
       * Does the necessary actions, when this as a query.
       */
      boolean handleQuery(JmDNSImpl dns, long expirationTime) {
         DNSRecord.Address dnsAddress = dns.getLocalHost().getDNSAddressRecord(this);
         if (dnsAddress != null) {
            if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && (!dnsAddress.sameValue(this))) {
   
                  Log.d(TAG, "handleQuery() Conflicting probe detected. dns state " + dns.getState() + " lex compare "
                           + lexCompare(dnsAddress));
         

               // Tie-breaker test
               if (dns.getState().isProbing() && lexCompare(dnsAddress) >= 0) {
                  // We lost the tie-break. We have to choose a different name.
                  dns.getLocalHost().incrementHostName();
                  dns.getCache().clear();
                  for (Iterator i = dns.getServices().values().iterator(); i.hasNext();) {
                     ServiceInfoImpl info = (ServiceInfoImpl) i.next();
                     info.revertState();
                  }
               }
               dns.revertState();
               return true;
            }
         }
         return false;
      }

      /**
       * Does the necessary actions, when this as a response.
       */
      boolean handleResponse(JmDNSImpl dns) {
         DNSRecord.Address dnsAddress = dns.getLocalHost().getDNSAddressRecord(this);
         if (dnsAddress != null) {
            if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && (!dnsAddress.sameValue(this))) {
               Log.d(TAG, "handleResponse() Denial detected");

               if (dns.getState().isProbing()) {
                  dns.getLocalHost().incrementHostName();
                  dns.getCache().clear();
                  for (Iterator i = dns.getServices().values().iterator(); i.hasNext();) {
                     ServiceInfoImpl info = (ServiceInfoImpl) i.next();
                     info.revertState();
                  }
               }
               dns.revertState();
               return true;
            }
         }
         return false;
      }

      DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out)
               throws IOException {
         return out;
      }

      public String toString() {
         return toString(" address '" + (addr != null ? addr.getHostAddress() : "null") + "'");
      }

   }

   /**
    * Pointer record.
    */
   public static class Pointer extends DNSRecord {
      String alias;

      public Pointer(String name, int type, int clazz, int ttl, String alias) {
         super(name, type, clazz, ttl);
         this.alias = alias;
      }

      void write(DNSOutgoing out) throws IOException {
         out.writeName(alias);
      }

      boolean sameValue(DNSRecord other) {
         return alias.equals(((Pointer) other).alias);
      }

      boolean handleQuery(JmDNSImpl dns, long expirationTime) {
         // Nothing to do (?)
         // I think there is no possibility for conflicts for this record type?
         return false;
      }

      boolean handleResponse(JmDNSImpl dns) {
         // Nothing to do (?)
         // I think there is no possibility for conflicts for this record type?
         return false;
      }

      String getAlias() {
         return alias;
      }

      DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out)
               throws IOException {
         return out;
      }

      public String toString() {
         return toString(alias);
      }
   }

   public static class Text extends DNSRecord {
      public byte text[];

      public Text(String name, int type, int clazz, int ttl, byte text[]) {
         super(name, type, clazz, ttl);
         this.text = text;
      }

      void write(DNSOutgoing out) throws IOException {
         out.writeBytes(text, 0, text.length);
      }

      boolean sameValue(DNSRecord other) {
         Text txt = (Text) other;
         if (txt.text.length != text.length) {
            return false;
         }
         for (int i = text.length; i-- > 0;) {
            if (txt.text[i] != text[i]) {
               return false;
            }
         }
         return true;
      }

      boolean handleQuery(JmDNSImpl dns, long expirationTime) {
         // Nothing to do (?)
         // I think there is no possibility for conflicts for this record type?
         return false;
      }

      boolean handleResponse(JmDNSImpl dns) {
         // Nothing to do (?)
         // Shouldn't we care if we get a conflict at this level?
         /*
          * ServiceInfo info = (ServiceInfo)
          * dns.services.get(name.toLowerCase()); if (info != null) { if (!
          * Arrays.equals(text,info.text)) { info.revertState(); return true; }
          * }
          */
         return false;
      }

      DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out)
               throws IOException {
         return out;
      }

      public String toString() {
         return toString((text.length > 10) ? new String(text, 0, 7) + "..." : new String(text));
      }
   }

   /**
    * Service record.
    */
   public static class Service extends DNSRecord {
      int priority;
      int weight;
      public int port;
      public String server;

      public Service(String name, int type, int clazz, int ttl, int priority, int weight, int port, String server) {
         super(name, type, clazz, ttl);
         this.priority = priority;
         this.weight = weight;
         this.port = port;
         this.server = server;
      }

      void write(DNSOutgoing out) throws IOException {
         out.writeShort(priority);
         out.writeShort(weight);
         out.writeShort(port);
         if (DNSIncoming.USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET) {
            out.writeName(server, false);
         } else {
            out.writeUTF(server, 0, server.length());

            // add a zero byte to the end just to be safe, this is the strange
            // form
            // used by the BonjourConformanceTest
            out.writeByte(0);
         }
      }

      private byte[] toByteArray() {
         try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream(bout);
            dout.write(name.getBytes("UTF8"));
            dout.writeShort(type);
            dout.writeShort(clazz);
            // dout.writeInt(len);
            dout.writeShort(priority);
            dout.writeShort(weight);
            dout.writeShort(port);
            dout.write(server.getBytes("UTF8"));
            dout.close();
            return bout.toByteArray();
         } catch (IOException e) {
            throw new InternalError();
         }
      }

      private int lexCompare(DNSRecord.Service that) {
         byte[] thisBytes = this.toByteArray();
         byte[] thatBytes = that.toByteArray();
         for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++) {
            if (thisBytes[i] > thatBytes[i]) {
               return 1;
            } else {
               if (thisBytes[i] < thatBytes[i]) {
                  return -1;
               }
            }
         }
         return thisBytes.length - thatBytes.length;
      }

      boolean sameValue(DNSRecord other) {
         Service s = (Service) other;
         return (priority == s.priority) && (weight == s.weight) && (port == s.port) && server.equals(s.server);
      }

      boolean handleQuery(JmDNSImpl dns, long expirationTime) {
         ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(name.toLowerCase());
         if (info != null && (port != info.port || !server.equalsIgnoreCase(dns.getLocalHost().getName()))) {
          
               Log.d(TAG, "handleQuery() Conflicting probe detected from: " + getRecordSource());
     
            
            DNSRecord.Service localService = new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV,
                     DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL, info.priority,
                     info.weight, info.port, dns.getLocalHost().getName());

            // This block is useful for debugging race conditions when jmdns is
            // respoding to
            // itself.
            try {
               if (dns.getInterface().equals(getRecordSource())) {
                  Log.d(TAG, "Got conflicting probe from ourselves\n" + "incoming: " + this.toString() + "\n"
                           + "local   : " + localService.toString());
               }
            } catch (IOException e) {
               e.printStackTrace();
            }

            int comparison = lexCompare(localService);

            if (comparison == 0) {
               // the 2 records are identical this probably means we are seeing
               // our own record.
               // With mutliple interfaces on a single computer it is possible
               // to see our
               // own records come in on different interfaces than the ones they
               // were sent on.
               // see section "10. Conflict Resolution" of mdns draft spec.
               Log.d(TAG, "handleQuery() Ignoring a identical service query");
               return false;
            }

            // Tie breaker test
            if (info.getState().isProbing() && comparison > 0) {
               // We lost the tie break
               String oldName = info.getQualifiedName().toLowerCase();
               info.setName(dns.incrementName(info.getName()));
               dns.getServices().remove(oldName);
               dns.getServices().put(info.getQualifiedName().toLowerCase(), info);
               Log.d(TAG, "handleQuery() Lost tie break: new unique name chosen:" + info.getName());

               // We revert the state to start probing again with the new name
               info.revertState();
            } else {
               // We won the tie break, so this conflicting probe should be
               // ignored
               // See paragraph 3 of section 9.2 in mdns draft spec
               return false;
            }

            return true;

         }
         return false;
      }

      boolean handleResponse(JmDNSImpl dns) {
         ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(name.toLowerCase());
         if (info != null && (port != info.port || !server.equalsIgnoreCase(dns.getLocalHost().getName()))) {
            Log.d(TAG, "handleResponse() Denial detected");

            if (info.getState().isProbing()) {
               String oldName = info.getQualifiedName().toLowerCase();
               info.setName(dns.incrementName(info.getName()));
               dns.getServices().remove(oldName);
               dns.getServices().put(info.getQualifiedName().toLowerCase(), info);
               Log.d(TAG, "handleResponse() New unique name chose:" + info.getName());

            }
            info.revertState();
            return true;
         }
         return false;
      }

      DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out)
               throws IOException {
         ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(name.toLowerCase());
         if (info != null) {
            if (this.port == info.port != server.equals(dns.getLocalHost().getName())) {
               return dns.addAnswer(in, addr, port, out, new DNSRecord.Service(info.getQualifiedName(),
                        DNSConstants.TYPE_SRV, DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, DNSConstants.DNS_TTL,
                        info.priority, info.weight, info.port, dns.getLocalHost().getName()));
            }
         }
         return out;
      }

      public String toString() {
         return toString(server + ":" + port);
      }
   }

   public void setRecordSource(InetAddress source) {
      this.source = source;
   }

   public InetAddress getRecordSource() {
      return source;
   }

   public String toString(String other) {
      return toString("record", ttl + "/" + getRemainingTTL(System.currentTimeMillis()) + "," + other);
   }

   public void setTtl(int ttl) {
      this.ttl = ttl;
   }

   public int getTtl() {
      return ttl;
   }
}
